Lock 和Condition

Lock

Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其 中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:

对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时, 如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。也就不会死锁。

如果我们重新设计一把互斥锁去解决这个问题,那该怎么设计呢?有三种方案。

  1. 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么 线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞 状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能 够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。

  2. 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个 错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

  3. 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线 程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

这三种方案可以全面弥补 synchronized 的问题。到这里相信你应该也能理解了,这三个方 案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法。

1
2
3
4
5
6
//支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(Long time,TimeUnit unit) throws InterruptedException;
//支持非阻塞获取锁的API
boolean tryLock();

如何保证可见性

Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{},需要重点关注 的是在 finally 里面释放锁。 一点需要解释一下,那就是可见性是怎么保证的。已经知道 Java 里多线程的可见性是通 过 Happens-Before 规则保证的,而 synchronized 之所以能够保证可见性,也是因为有 一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁 的加锁。那 Java SDK 里面 Lock 靠什么保证可见性呢?利用了volatile相关的Happens-Before规则

可重入锁

ReentrantLock,这个翻译过来叫可 重入锁,。所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁。例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入 的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class X{
private final Lock rt1 =new ReentrantLock();
int value;
public int get(){
//获取锁
rt1.lock(); @2
try{
return value;
}finally{
//保证锁释放
rt1.unlock();
}
}
public void addOne(){
//获取锁
rt1.lock();
try{
value =1+get(); @1
}finally{
//保证锁能释放
rt1.unlock();
}
}
}

公平锁和非公平锁

在使用 ReentrantLock 的时候,会发现 ReentrantLock 这个类有两个构造函数,一个是 无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传 入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。

在前面管程章节中,我们介绍过入口等待队列,锁都对应着一 个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需 要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就 唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。

最佳实践

  1. 永远只在更新对象的成员变量时加锁
  2. 永远只在访问可变的成员变量时加锁
  3. 永远不在调用其他对象的方法时加锁

Condition

Condition 实现了管程模型里面的条件变量。

Java 语言内置的管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,这是二者的一个重要区别。在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容 易。例如,实现一个阻塞队列,就需要两个条件变量。

利用条件变量实现阻塞队列

一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),一个是队列不满(队列已满不允许入队)。

Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。 但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、 signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才 能使用。

Java SDK 并发包里的 Lock 和 Condition 不过就是管程的一种实现而已。

同步异步

俗点来讲就是调用方是否需要等待结果,如果需要等待结果,就是同步;如 果不需要等待结果,就是异步。

同步,是Java代码默认的处理方式,如果程序想支持异步,可以通过以下两种方式:

  1. 调用方创建一个子线程,在子线程中执行方法调用,这种调用我们称为异步调用;

  2. 方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接 return,这种方法我们 一般称为异步方法。